# -*- coding:utf-8 -*-
import tkinter as tk
from tkinter import ttk
import tkinter.filedialog,tkinter.messagebox
import hashlib
import ctypes,sys

#检查AES包是否存在
try:
    from Crypto.Cipher import AES
except ImportError:
    tk.messagebox.showwarning('提示','''我们找不到加解密用的库了…
如果你需要使用这个功能，请在命令提示符中输入pip install pycryptodome(如果您有pip的话)
当然，这个程序仍然可以运行，但加解密文件功能将失效''')
    warns = '''我们找不到加解密用的库了…
如果你需要使用这个功能，请在命令提示符中输入pip install pycryptodome(如果您有pip的话)'''    #如果强行调用会弹出的提示
    ENABLE_AES = True
else:
    ENABLE_AES = False


class TextEditor(object):
    '''文本阅读器
        通过ContactEditor的“帮助”菜单可以激活'''
    def mainscreen(self,widget = True):
        self.root = tk.Tk()

        # 进行高清修复
        if sys.platform.startswith('win'):
            ctypes.windll.shcore.SetProcessDpiAwareness(1)
            # 获取当前显示器的缩放率（以%为单位，比如返回200就是200%的缩放）
            ScaleFactor = ctypes.windll.shcore.GetScaleFactorForDevice(0)
            # 由缩放率计算出dpi并调用tk的缩放函数
            # 每1%的缩放率对应1/72的dpi。也就是说，正常100%缩放率屏幕的dpi为72
            self.root.tk.call('tk', 'scaling', ScaleFactor / 72)
        
        self.root.title('文本编辑器')
        self.root.protocol('WM_DELETE_WINDOW',self._exit)

        if widget:
            self.root.bind_all('<Control-n>',self.newfile)
            self.root.bind_all('<Control-o>',self.openfile)
            self.root.bind_all('<Control-s>',self.save)
            self.root.bind_all('<Control-Shift-s>',self._saveasfile)
            self.root.bind_all('<Control-z>',self.undo)
            self.root.bind_all('<Control-y>',self.redo)
            self.root.bind_all('<F1>',load_help)
            self.lb = tk.Frame(self.root)
            lb = tk.Label(self.lb,text = 'TextEditor',font = ('Cambria',24),fg = '#ff26e4',width = 15,height = 3)
            lb.pack()
            ttk.Button(self.lb,text = '新建',command = self.newfile).pack()
            ttk.Button(self.lb,text = '打开',command = self.openfile).pack()
            self.lb.pack()
        

    def createmenu(self):
        self.menubar = tk.Menu(self.root)
        self.filebar = tk.Menu(self.menubar,tearoff = False)
        self.filebar.add_command(label = '新建',accelerator = 'Crtl+N',command = self.newfile)
        self.filebar.add_command(label = '打开…',accelerator = 'Ctrl+O',command = self.openfile)
        self.filebar.add_command(label = '保存',accelerator = 'Ctrl+S',state = 'disable',command = self.save)
        self.filebar.add_command(label = '另存为…',accelerator = 'Ctrl+Shift+S',state = 'disable',command = self._saveasfile)
        self.filebar.add_command(label = '退出',command = self._exit)

        self.editbar = tk.Menu(self.menubar,tearoff = False)
        self.editbar.add_command(label = '撤销',accelerator = 'Ctrl+Z',command = self.undo,state = 'disable')
        self.editbar.add_command(label = '恢复',accelerator = 'Ctrl+Y',command = self.redo,state = 'disable')

        helpbar = tk.Menu(self.menubar,tearoff = False)
        helpbar.add_command(label = '使用帮助…',accelerator = 'F1',command = load_help)

        self.menubar.add_cascade(label = '文件',menu = self.filebar)
        self.menubar.add_cascade(label = '编辑',menu = self.editbar)
        self.menubar.add_cascade(label = '帮助',menu = helpbar)
        
        self.root.config(menu = self.menubar)

    def _changed(self):
        '''测试编辑模式下是否有更改，被_exit，save方法调用'''
        if not hasattr(self,'md5'):
            return False
        if not hasattr(self,'t'):
            return False
        if MD5(self.t.get(1.0,tk.END)) == self.md5:
            return False
        else:
            return True
        
    def undo(self,event = None):
        try:
            self.t.edit_undo()
        except (AttributeError,tk._tkinter.TclError):return

    def redo(self,event = None):
        try:
            self.t.edit_redo()
        except (AttributeError,tk._tkinter.TclError):return

    def newfile(self,event = None):
        if self._changed():
            cho = tk.messagebox.askyesnocancel('提示','您在新建文件前前要保存更改吗？')
            if cho is None: return
            if cho:
                self.save()
        self.destroy()
            
        self.frame = tk.Frame(self.root)
        self.t = tk.Text(self.frame,undo = True)
        scroll = tk.Scrollbar(self.frame,command = self.t.yview)
        self.t.configure(yscrollcommand = scroll.set)
        scroll.pack(side = 'right',fill = tk.Y)
        self.t.pack(side = 'left')
        self.frame.pack()
        self.md5 = b'h\xb3)\xda\x98\x93\xe3@\x99\xc7\xd8\xad\\\xb9\xc9@'      #空内容的MD5值
        self.root.title('文本编辑器 - *untitled*')

        try:
            del self.filename
        except AttributeError:pass       #否则直接新建文件会出错
        
        self.filebar.entryconfig(index = 2,state = 'normal')
        self.filebar.entryconfig(index = 3,state = 'normal')
        self.editbar.entryconfig(index = 0,state = 'normal')
        self.editbar.entryconfig(index = 1,state = 'normal')
    
    def openfile(self,event = None):
        if self._changed():
            cho = tk.messagebox.askyesnocancel('提示','您在打开新文件前要保存更改吗？')
            if cho is None: return
            if cho:
                self.save()

        if not ENABLE_AES:       
            filename = tk.filedialog.askopenfilename(filetypes = [('Text file(*.txt)','*.txt'),
                                                                  ('Secret text file(*.sec)','*.sec'),
                                                                  ('Python file(*.py)','*.py'),
                                                                    ('All files(*.*）','*.*')])
        else:
            filename = tk.filedialog.askopenfilename(defaultextension = ".txt",
                                   filetypes = [('Text files(*.txt)',"*.txt"),
                                                ('Python file(*.py)','*.py'),
                                                ('All files(*.*)',"*.*")]
                                   )
                                                            
        if not filename:
            return
        self.root.title('文本编辑器  -'+filename)
        if filename.endswith(".sec"):
            if ENABLE_AES:
                tk.messagebox.showwarning('提示',warns)
                return
            else:
                self.destroy()
                try:
                    self.desec(filename)
                except ValueError:
                    self.lb.pack()
                    self.root.title('文本编辑器')
        else:
            if filename.endswith('.py'):
                if not tk.messagebox.askyesno('提示','您正在打开python代码文件，建议使用IDLE打开以'
                                       '获得更好的体验，是否继续？'):
                    return
            self.destroy()
            self.loaddata(filename)

    def destroy(self):
        '''存放在打开文件时所需的清理代码，以免出现重复的窗口部件'''
        if hasattr(self,'lb'):
            self.lb.pack_forget()   
        if hasattr(self,'frame'):
            self.frame.destroy()
        if hasattr(self,'modebar'):
            self.modebar.destroy()

    def _exit(self):
        '''接管关闭窗口的请求，用于提示保存更改'''
        if self._changed():
            cho = tk.messagebox.askyesnocancel('提示','您在退出前要保存更改吗？')
            if cho is None: return
            if cho:
                self.save()
        self.root.destroy()

    def desec(self,filename):
        file = b''
        with open(filename,'rb') as f:
            for one in f:
                file += one

        file = file.split(b'\x0ced',1)
        
        self.askforpassword(head = file[0])
        self.root.wait_window(self.tp)
        pw = self.pw.get()
        if not pw:
            raise ValueError('用户取消了输入')
        else:
            text = self.decrypt(password = pw,text = file[1]).split('\n')
            self._loaddata(text = text,filename = filename)
        

    def decrypt(self,password,text):
        '''解密一段文本'''
        aes = AES.new(to16(password),mode = AES.MODE_ECB)
        light = aes.decrypt(text).rstrip(b'\x00')

        return light.decode()
        
    def encrypt(self,password,text):
        '''加密一段文本'''
        aes = AES.new(to16(password),mode = AES.MODE_ECB)
        secret = aes.encrypt(to16(text))
        pw = MD5(password)

        return pw+b'\x0ced'+secret
        
         
    def loaddata(self,filename,edit = True):
        '''主要实现编码自动选择'''
        codes = ['gbk','utf-8']    #储存编码的列表：可以再添加几个
        firstcode = codes[0]
        
        while True:          #实现编码自动选择的循环
            file = open(filename,encoding = codes[0])
            self.frame = tk.Frame(self.root)
            self.t = tk.Text(self.frame)
            try:
                for one in file:
                    self.t.insert(tk.END,one)
            except UnicodeDecodeError:
                codes = codes[1:]+codes[:1]
                self.t.destroy()
                if codes[0] == firstcode:
                    tk.messagebox.showerror('提示','这个文件的编码有点奇怪，我们打不开它了…')
                    self.lb.pack()
                    self.root.title('文本编辑器')
                    return
                else:
                    continue
            else:
                break

        self._loaddata(filename,edit)

    def _loaddata(self,filename,edit = True,text = None):
        '''内部加载tkinter的部分'''
        if text is not None:
            self.frame = tk.Frame(self.root)
            self.t = tk.Text(self.frame)
            for one in text:
                one += '\n'
                self.t.insert(tk.END,one)
        
        self.filename = filename
        sc = tk.Scrollbar(self.frame,command = self.t.yview)
        self.t.configure(state = 'disable',yscrollcommand = sc.set)
        self.t.bind('<Key>',self.title_uncomplete)
        self.title_add = False
        self.t.pack(side = 'left')
        sc.pack(side = 'right',fill = tk.Y)

        if edit:
            self.modebar = tk.Frame(self.root)
            tk.Label(self.modebar, text = '当前处于阅读模式', font = ('黑体',10)).pack(side = 'left')
            ttk.Button(self.modebar, text = '启用编辑',command = self.startedit).pack(side = 'left',ipadx = 10)
            self.modebar.pack()

        self.frame.pack()
        self.md5 = MD5(self.t.get(1.0,tk.END))
        self.t.focus()

    def startedit(self):
        '''被loaddata中的’启用编辑‘调用'''
        self.modebar.destroy()
        self.t.configure(state = 'normal',undo = True)    #良心Text自带undo/redo……上次ContactEditor害我好苦
        self.filebar.entryconfig(index = 2,state = 'normal')
        self.filebar.entryconfig(index = 3,state = 'normal')
        self.editbar.entryconfig(index = 0,state = 'normal')
        self.editbar.entryconfig(index = 1,state = 'normal')

    def title_uncomplete(self,event):
        '''为未保存的文件在标题上添加‘*’号'''       
        if self.title_add:return

        self.t.insert('insert',event.char)
            
        title = self.root.title().split(' ',1)
        title = title[0]+' *'+title[1]
        self.root.title(title)
        self.title_add = True
        if not self._changed():
            self.title_complete()         #处理有人写了点东西又删了的情况:
        self.t.delete("%s-1c" %"insert" , "insert")


    def title_complete(self,event = None):
        '''去掉标题里的‘*‘号'''
        self.root.title('文本编辑器  -'+self.filename)
        self.title_add = False

    def askpassword(self):
        def confirm(event = None):
            s = self.s.get()
            if len(s) >= 8:
                tk.messagebox.showinfo('提醒',f'您设定的密码是 {s} ,请牢记！')
                self.tp.destroy()
            else:
                tk.messagebox.showwarning('提示','密码需要至少八位哦',parent = self.tp)
                pwframe.focus()

        def cancel():
            self.s.set('')
            self.tp.destroy()

        self.tp = tk.Toplevel(self.root)
        self.tp.title('设置密码')
        self.tp.bind('<Return>',confirm)
        self.tp.protocol('WM_DELETE_WINDOW',cancel)
        
        self.s = tk.StringVar()
        pwframe = tk.Frame(self.tp)
        tk.Label(pwframe,text = '请输入加密密码').pack(side = 'left')
        tk.Entry(pwframe,textvariable = self.s,show = '*').pack(side = 'right',ipadx = 5)

        btframe = tk.Frame(self.tp)
        ttk.Button(btframe,text = '确认',command = confirm).pack(side = 'left')
        ttk.Button(btframe,text = '取消',command = cancel).pack(side = 'right',ipadx = 10)

        pwframe.pack()
        btframe.pack()
        pwframe.focus()

    def askforpassword(self,head):
        '''询问解密密码'''
        def confirm(event = None):
            pw = self.pw.get()
            if not MD5(pw) == head:
                tk.messagebox.showerror('哎呀','输入的密码不正确',parent = self.tp)
                pwframe.focus()
                
            else:
                self.tp.destroy()

        def cancel():
            self.pw.set('')
            self.tp.destroy()

        self.tp = tk.Toplevel(self.root)
        self.tp.title('输入密码')
        self.tp.bind('<Return>',confirm)
        self.tp.protocol('WM_DELETE_WINDOW',cancel)

        self.pw = tk.StringVar()
        pwframe = tk.Frame(self.tp)
        tk.Label(pwframe,text = '请输入解密密码').pack(side = 'left')
        tk.Entry(pwframe,textvariable = self.pw,show = '*').pack(side = 'right',ipadx = 5)

        btframe = tk.Frame(self.tp)
        ttk.Button(btframe,text = '确认',command = confirm).pack(side = 'left')
        ttk.Button(btframe,text = '取消',command = cancel).pack(side = 'right',ipadx = 10)

        pwframe.pack()
        btframe.pack()
        pwframe.focus()
    
    def save(self,event = None):
        '''处理文件保存，将任务分给_save或_savefile执行'''
        if not self._changed():
            return
        
        else:
            if hasattr(self,'filename'):
                self._save()
            else:
                self._saveasfile()
                
    def _save(self):
        '''处理已有文件的保存（不过也是全部覆盖）'''
        if self.filename.endswith('.txt'):
            with open(self.filename,'w') as f:
                f.write(self.t.get(1.0,tk.END))
                
        elif self.filename.endswith('.sec'):
            text = self.t.get(1.0,tk.END)
            pw = self.pw.get()
            text = self.encrypt(pw,text)
            with open(self.filename,'wb') as f:
                f.write(text)
            
        self.md5 = MD5(self.t.get(1.0,tk.END))
        self.title_complete()

    def _saveasfile(self):
        '''处理新建的文件的保存'''
        if not ENABLE_AES:
            filename = tk.filedialog.asksaveasfilename(defaultextension = ".txt",
                                                           filetypes = [('Text files(*.txt)',"*.txt"),
                                                                        ('Secret text file(*.sec)',"*.sec"),
                                                                        ('Python file(*.py)','*.py'),
                                                                        ('All files(*.*)',"*.*")])
        else:
            filename = tk.filedialog.asksaveasfilename(defaultextension = ".txt",
                                               filetypes = [('Text files(*.txt)',"*.txt"),
                                                            ('Python file(*.py)','*.py'),
                                                            ('All files(*.*)',"*.*")]
                                               )

            
        if not filename: return

        if filename.endswith('.sec'):
            if ENABLE_AES:
                tk.messagebox.showwarning('提示',warns)
                return
            
            self.askpassword()
            self.root.wait_window(self.tp)
            if not self.s.get():
                return
            else:
                text = self.encrypt(self.s.get(),self.t.get(1.0,tk.END))
                with open(filename,'wb') as f:
                    f.write(text)

        else:
            text = self.t.get(1.0,tk.END)
            with open(filename,"w",encoding = 'utf-8') as f:
                f.write(text)     #TODO;提高性能
                #这可能在修改大文件时很耗时！因为它重写了整个文件
                    
        self.md5 = MD5(self.t.get(1.0,tk.END))
        self.root.title('文本编辑器 -'+filename)
        self.filename = filename
                    
        
    def start(self,event = None,file = None):
        self.mainscreen()
        self.createmenu()
        if file is not None:
            self.loaddata(filename = file)
        self.root.mainloop()

    def simplestart(self,file,edit = False,title = None):
        self.mainscreen(widget = False)
        try:
            self.lb.destroy()
        except (tk._tkinter.TclError,AttributeError):pass
        
        self.loaddata(filename = file,edit = edit)
        
        if title is not None:
            self.root.title(title)
            
        self.root.mainloop()


def MD5(text):
    hash = hashlib.md5()
    hash.update(text.encode())
    return hash.digest()


def to16(text):
    try:
        text = str(text).encode()
    except AttributeError:
        if isinstance(text,bytes):
            pass
        else:
            return
    else:
        while len(text) % 16 != 0:
            text += b'\x00'

    return text
    
def load_help(event = None):
    try:
        TextEditor().simplestart(file = 'Help-TextEditor.txt',title = 'TextEditor -帮助')
    except FileNotFoundError:
        tk.messagebox.showwarning('啊哦','对不起，我们找不着帮助文件了…')

if __name__ == '__main__':
    TextEditor().start()
